# JVM 规范完整指南 - Java 虚拟机的体系结构。

本章节基于 [JDK 25 (2025)](https://docs.oracle.com/javase/specs/index.html) 同步发布的 JVM 规范进行翻译学习

## 2. Java 虚拟机结构

本文档规范的是一个抽象机器。它并未描述 Java 虚拟机的任何具体实现。

要正确实现 Java 虚拟机，您只需能够读取 class 文件格式并正确执行其中规定的操作即可。那些不属于 Java 虚拟机规范范畴的实现细节，若加以规定，将会不必要地限制实现者的创造力。例如，运行时数据区的内存布局、所使用的垃圾回收算法，以及任何对 Java 虚拟机指令进行的内部优化（例如，将其翻译为机器代码），都交由实现者自行决定。

本规范中所有对 Unicode 的引用，均依据 The Unicode Standard, Version 16.0.0，该版本可在[https://www.unicode.org/](https://www.unicode.org/)获取。

### 2.1 Class 文件格式（The class File Format）

将由 Java 虚拟机执行的已编译代码，使用一种独立于硬件和操作系统的二进制格式来表示，这种格式通常（但非必须）存储在文件中，被称为 Class 文件格式。Class 文件格式精确定义了类或接口的表示形式，包括诸如字节顺序等在特定平台的对象文件格式中可能被视为理所当然的细节。

第四章 "Class 文件格式" 将详细阐述 Class 文件格式。

### 2.2 数据类型（Data Types）

与 Java 编程语言类似，Java 虚拟机操作两种类型：基本类型 和 引用类型。相应地，也有两种类型的值可以存储在变量中、作为参数传递、被方法返回和进行操作：基本值 和 引用值。

Java 虚拟机期望几乎所有的类型检查都在运行期之前完成（通常由编译器执行），而不必由 Java 虚拟机自身来完成。基本类型的值不需要被标记，也无需在运行时通过检查来确定其类型，或者与引用类型的值进行区分。相反，Java 虚拟机的指令集通过使用专用于操作特定类型值的指令来区分其操作数类型。例如，iadd、ladd、fadd 和 dadd 都是用于将两个数值相加并产生数值结果的 Java 虚拟机指令，但每条指令分别专用于其操作数类型：int、long、float 和 double。有关 Java 虚拟机指令集中类型支持的摘要，请参见 §2.11.1。

Java 虚拟机包含对对象的显式支持。一个对象可以是一个动态分配的类实例，也可以是一个数组。对一个对象的引用被视为具有 Java 虚拟机类型 reference。引用是多态的：一个单一的引用也可以是多个类类型、接口类型或数组类型的值。reference 类型的值可以被视为指向对象的指针。一个对象可能存在多个引用。对象总是通过 reference 类型的值来进行操作、传递和测试。

### 2.3 基本类型与值（Primitive Types and Values）

Java 虚拟机支持的基本数据类型是数字类型、布尔类型（§2.3.4）和 returnAddress 类型（§2.3.3）。

数字类型包括整数类型（§2.3.1）和浮点类型（§2.3.2）。

**整数类型包括：**

- byte： 值为 8 位有符号二进制补码整数，默认值为零。
- short： 值为 16 位有符号二进制补码整数，默认值为零。
- int： 值为 32 位有符号二进制补码整数，默认值为零。
- long： 值为 64 位有符号二进制补码整数，默认值为零。
- char： 值为 16 位无符号整数，表示基本多文种平面中的 Unicode 码点，采用 UTF-16 编码，默认值为空码点 ('\u0000')。

**浮点类型包括：**

- float： 其值精确对应于 32 位 IEEE 754 二进制 32 格式可表示的值，默认值为正零。
- double： 其值精确对应于 64 位 IEEE 754 二进制 64 格式可表示的值，默认值为正零。

**布尔类型**的值编码真值 true 和 false，默认值为 false。

《Java® 虚拟机规范（第一版）》并未将 boolean 视为一种 Java 虚拟机类型。然而，布尔值在 Java 虚拟机中确实得到了有限的支持。《Java® 虚拟机规范（第二版）》通过将 boolean 视为一个类型，澄清了这个问题。

returnAddress 类型的值是指向 Java 虚拟机指令操作码的指针。在基本类型中，只有 returnAddress 类型与 Java 编程语言类型没有直接关联。

#### 2.3.1 整数类型与值（Integral Types and Values）

Java 虚拟机中整数类型的取值范围如下：

- byte：从 -128 到 127（包含），即 -2⁷ 到 2⁷ - 1
- short：从 -32768 到 32767（包含），即 -2¹⁵ 到 2¹⁵ - 1
- int：从 -2147483648 到 2147483647（包含），即 -2³¹ 到 2³¹ - 1
- long：从 -9223372036854775808 到 9223372036854775807（包含），即 -2⁶³ 到 2⁶³ - 1
- char：从 0 到 65535（包含）

#### 2.3.2 浮点类型与值（Floating-Point Types and Values）

浮点类型包括 float 和 double，它们在概念上与 IEEE 754 值和操作的 32 位 binary32 和 64 位 binary64 浮点格式相关联，具体规范见 IEEE 754 标准 (JLS §1.7)。

在 Java SE 15 及之后版本中，Java 虚拟机使用 2019 版 IEEE 754 标准。在 Java SE 15 之前，Java 虚拟机使用 1985 版 IEEE 754 标准，其中 binary32 格式称为单精度格式，binary64 格式称为双精度格式。

IEEE 754 不仅包括由符号和量值组成的正数和负数，还包括正零和负零、正无穷大和负无穷大，以及特殊的 NaN 值。NaN 值用于表示某些无效操作的结果，例如零除以零。float 和 double 类型的 NaN 常量已预定义为 Float.NaN 和 Double.NaN。

浮点类型的有限非零值都可以表示为 s · m · 2^(e - N + 1) 的形式，其中：

- s 是 +1 或 -1，
- m 是小于 2^N 的正整数，
- e 是介于 Emin = -(2^(K-1)-2) 和 Emax = 2^(K-1)-1（包含）之间的整数，
- N 和 K 是依赖于类型的参数。

有些值可以用多种方式以此形式表示。例如，假设一个浮点类型的值 v 可以用特定的 s、m 和 e 值以此形式表示，那么如果 m 恰好是偶数且 e 小于 2^(K-1)，则可以将 m 减半并将 e 加 1，从而产生同一值 v 的第二种表示形式。

如果 m ≥ 2^(N-1)，则此形式的表示称为规范化表示；否则，该表示称为非规范化。如果一个浮点类型的值无法以 m ≥ 2^(N-1) 的方式表示，则该值称为非规约值，因为其量值小于最小规范化值的量值。

float 和 double 的参数 N 和 K（以及派生参数 Emin 和 Emax）的约束条件总结在表 2.3.2-A 中。

**表 2.3.2-A. 浮点参数**

| 参数     | float | double |
| :------- | :---- | :----- |
| **N**    | 24    | 53     |
| **K**    | 8     | 11     |
| **Emax** | +127  | +1023  |
| **Emin** | -126  | -1022  |

除了 NaN，浮点值是有序的。按从小到大的顺序排列，它们分别是：负无穷大、负有限非零值、正零和负零、正有限非零值和正无穷大。

IEEE 754 允许其 binary32 和 binary64 浮点格式有多种不同的 NaN 值。然而，Java SE 平台通常将给定浮点类型的 NaN 值视为折叠成了一个单一的规范值，因此本规范通常将任意的 NaN 当作一个规范值来引用。

根据 IEEE 754，一个使用非 NaN 操作数的浮点运算可能会产生 NaN 结果。IEEE 754 规定了一组 NaN 位模式，但并未强制要求使用哪个特定的 NaN 位模式来表示一个 NaN 结果；这由硬件架构决定。程序员可以创建具有不同位模式的 NaN 来编码（例如）回溯性诊断信息。这些 NaN 值可以分别通过 Float.intBitsToFloat 和 Double.longBitsToDouble 方法来创建 float 和 double 类型的 NaN。相反地，要检查 NaN 值的位模式，可以分别使用 Float.floatToRawIntBits 和 Double.doubleToRawLongBits 方法。

正零和负零在比较时是相等的，但存在其他操作可以区分它们；例如，将 1.0 除以 0.0 会得到正无穷大，而将 1.0 除以 -0.0 会得到负无穷大。

NaN 是无序的，因此如果数值比较和数值相等性测试的两个操作数中有一个或两个是 NaN，那么其结果值就是 false。特别地，一个值与其自身的数值相等性测试结果为 false，当且仅当该值是 NaN。而数值不等性测试，如果任一操作数是 NaN，则结果值为 true。

#### 2.3.3 returnAddress 类型和值（ The returnAddress Type and Values）

returnAddress 类型由 Java 虚拟机的 jsr、ret 和 jsr_w 指令（§jsr, §ret, §jsr_w）使用。returnAddress 类型的值是指向 Java 虚拟机指令操作码的指针。

与数字基本类型不同，returnAddress 类型不对应于任何 Java 编程语言类型，并且不能被运行中的程序修改。

#### 2.3.4 boolean 类型（The boolean Type）

尽管 Java 虚拟机定义了 boolean 类型，但它仅为其提供了非常有限的支持。没有专门用于操作 boolean 值的 Java 虚拟机指令。相反，Java 编程语言中操作 boolean 值的表达式会被编译为使用 Java 虚拟机 int 数据类型的值。

Java 虚拟机直接支持 boolean 数组。其 newarray 指令（§newarray）允许创建 boolean 数组。boolean 类型的数组使用字节数组指令 baload 和 bastore（§baload, §bastore）来进行访问和修改。

在 Oracle 的 Java 虚拟机实现中，Java 编程语言中的 boolean 数组被编码为 Java 虚拟机的字节数组，每个 boolean 元素使用 8 位。

Java 虚拟机使用 1 表示 true，0 表示 false 来编码 boolean 数组元素。在 Java 编程语言的 boolean 值由编译器映射到 Java 虚拟机 int 类型值的情况下，编译器必须使用相同的编码。

### 2.4 引用类型与值（Reference Types and Values）

引用类型有三种：类类型、数组类型和接口类型。它们的值分别是对动态创建的类实例、数组，或者实现了接口的类实例或数组的引用。

一个数组类型由一个具有单一维度（其长度不由类型给出）的组件类型构成。数组类型的组件类型本身可以是一个数组类型。如果从任意数组类型开始，考虑其组件类型，然后（如果该组件类型也是数组类型）再考虑该类型的组件类型，如此继续，最终必定会到达一个不是数组类型的组件类型；这被称为该数组类型的元素类型。数组类型的元素类型必须是一个基本类型、类类型或接口类型。

### 2.5 运行时数据区（Run-Time Data Areas）

Java 虚拟机定义了若干在程序执行期间使用的运行时数据区。其中一些数据区在 Java 虚拟机启动时创建，并且仅在 Java 虚拟机终止时才会被销毁。其他数据区则是每个线程独有的。每个线程独有的数据区在线程创建时被创建，在线程终止时被销毁。

#### 2.5.1 程序计数器（The pc Register）

Java 虚拟机可以同时支持多个执行线程（JLS §17）。每个 Java 虚拟机线程都有其自己的 pc（程序计数器）寄存器。在任何时刻，每个 Java 虚拟机线程都在执行单个方法的代码，即该线程的当前方法（§2.6）。如果该方法是非本地（non-native） 的，则 pc 寄存器包含当前正在执行的 Java 虚拟机指令的地址。如果线程当前正在执行的方法是本地（native） 的，则 Java 虚拟机的 pc 寄存器的值是未定义的。Java 虚拟机的 pc 寄存器足够宽，可以容纳一个 returnAddress 类型值或特定平台上的一个本地指针。

#### 2.5.2 Java 虚拟机栈（ Java Virtual Machine Stacks）

每个 Java 虚拟机线程都有一个私有的 Java 虚拟机栈，该栈在线程创建时同时被创建。Java 虚拟机栈用于存储栈帧（§2.6）。Java 虚拟机栈类似于传统语言（如 C）中的栈：它存储局部变量和部分结果，并参与方法的调用与返回。由于 Java 虚拟机栈除了压入和弹出栈帧外，不会被直接操作，因此栈帧可以在堆上分配。Java 虚拟机栈的内存空间不需要是连续的。

在《Java® 虚拟机规范（第一版）》中，Java 虚拟机栈被称为 Java 栈。

本规范允许 Java 虚拟机栈可以是固定大小的，也可以根据计算需要动态扩展和收缩。如果 Java 虚拟机栈是固定大小的，那么每个栈的大小可以在该栈创建时独立选择。

Java 虚拟机实现可以为程序员或用户提供对 Java 虚拟机栈初始大小的控制，对于可动态扩展和收缩的 Java 虚拟机栈，还可以控制其最大和最小大小。

#### 2.5.3 堆（Heap）

Java 虚拟机拥有一个在所有 Java 虚拟机线程之间共享的堆。堆是运行时数据区，所有类实例和数组的内存都在这里分配。

堆在虚拟机启动时创建。对象的堆存储由一个自动存储管理系统（即垃圾收集器）进行回收；对象永远不会被显式释放。Java 虚拟机不假定任何特定类型的自动存储管理系统，可以根据实现者的系统要求选择存储管理技术。堆可以是固定大小的，也可以根据计算需要扩展，如果不需要较大的堆，也可以收缩。堆的内存不需要是连续的。

Java 虚拟机实现可以为程序员或用户提供对堆初始大小的控制，对于可以动态扩展和收缩的堆，还可以控制堆的最大和最小大小。

**以下异常情况与堆相关：**

- 如果计算需要的堆空间超过了自动存储管理系统能够提供的量，Java 虚拟机将抛出 OutOfMemoryError。

#### 2.5.4 方法区（Method Area）

Java 虚拟机拥有一个在所有 Java 虚拟机线程之间共享的方法区。方法区类似于传统语言的编译代码存储区，或类似于操作系统进程中的"文本"段。它存储每个类的结构信息，例如运行时常量池、字段和方法数据，以及方法和构造器的代码，包括用于类和接口初始化以及实例初始化（§2.9）的特殊方法。

方法区在虚拟机启动时创建。尽管方法区在逻辑上是堆的一部分，但简单的实现可以选择不对其进行垃圾收集或压缩。本规范并不强制规定方法区的具体位置或用于管理已编译代码的策略。方法区可以是固定大小的，也可以根据计算需要扩展，如果不需要较大的方法区，也可以收缩。方法区的内存不需要是连续的。

Java 虚拟机实现可以为程序员或用户提供对方法区初始大小的控制，对于大小可变化的方法区，还可以控制其最大和最小大小。

**以下异常情况与方法区相关：**

- 如果方法区中的内存无法满足分配请求，Java 虚拟机将抛出 OutOfMemoryError。

#### 2.5.5 运行时常量池（Run-Time Constant Pool）

运行时常量池是类文件中 constant_pool 表（§4.4）的每个类或每个接口的运行时表示形式。它包含多种常量，范围从编译期可知的数值字面量到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于传统编程语言中的符号表，尽管它包含的数据范围比典型的符号表更广。

每个运行时常量池都是从 Java 虚拟机的方法区（§2.5.4）中分配的。类或接口的运行时常量池是在 Java 虚拟机创建该类或接口时（§5.3）构建的。

**以下异常情况与类或接口的运行时常量池的构建相关：**

- 在创建类或接口时，如果构建运行时常量池需要的内存超过了 Java 虚拟机方法区所能提供的容量，Java 虚拟机将抛出 OutOfMemoryError。

有关运行时常量池构建的信息，请参见 §5（加载、链接和初始化）。

#### 2.5.6 本地方法栈（Native Method Stacks）

Java 虚拟机实现可能会使用传统的栈，通常称为 "C 栈"，来支持本地方法（即使用非 Java 编程语言编写的方法）。Java 虚拟机指令集的解释器实现（例如使用 C 语言实现）也可能使用本地方法栈。那些无法加载本地方法且自身不依赖传统栈的 Java 虚拟机实现，则无需提供本地方法栈。如果提供了本地方法栈，那么通常会在每个线程创建时为其分配相应的本地方法栈。

本规范允许本地方法栈可以是固定大小的，也可以根据计算需要动态扩展和收缩。如果本地方法栈是固定大小的，那么每个栈的大小可以在该栈创建时独立选择。

Java 虚拟机实现可以为程序员或用户提供对本地方法栈初始大小的控制，对于大小可变的本地方法栈，还可以控制其最大和最小大小。

**以下异常情况与本地方法栈相关：**

- 如果线程中的计算需要比允许的更大的本地方法栈，Java 虚拟机将抛出 StackOverflowError。
- 如果本地方法栈可以动态扩展，并且在尝试扩展时无法获得足够的内存，或者无法获得足够的内存来为新线程创建初始的本地方法栈，则 Java 虚拟机将抛出 OutOfMemoryError。

### 2.6 栈帧（Frames）

栈帧用于存储数据和部分结果，并用于执行动态链接、为方法提供返回值以及派发异常。

每次调用方法时都会创建一个新的栈帧。当方法调用完成时（无论是正常完成还是突然完成，即抛出未捕获的异常），其对应的栈帧就会被销毁。栈帧是从创建该栈帧的线程的 Java 虚拟机栈（§2.5.2）中分配的。每个栈帧都有自己的局部变量表（§2.6.1）、自己的操作数栈（§2.6.2），以及一个指向当前方法所属类的运行时常量池（§2.5.5）的引用。

一个栈帧可能会被扩展，以包含一些额外的、特定于实现的信息，例如调试信息。

局部变量表和操作数栈的大小在编译期确定，并与该栈帧所关联方法的代码一起提供（§4.7.3）。因此，栈帧数据结构的大小仅取决于 Java 虚拟机的实现，并且这些结构所需的内存在方法调用时可以同时分配。

在某个控制线程中的任一时刻，只有一个栈帧是活动的，即正在执行的方法的栈帧。这个栈帧被称为当前帧，其方法被称为当前方法。定义当前方法的类被称为当前类。对局部变量和操作数栈的操作通常都是针对当前帧的。

当一个帧的方法调用了另一个方法或其方法完成时，它就不再是当前帧。当调用一个方法时，会创建一个新帧，并在控制权转移到新方法时，该新帧成为当前帧。在方法返回时，当前帧将其方法调用的结果（如果有）传回给前一帧，随后当前帧被丢弃，而前一帧则重新成为当前帧。

请注意，由线程创建的栈帧是该线程本地的，不能被任何其他线程引用。

#### 2.6.1 局部变量表（Local Variables）

每个栈帧（§2.6）都包含一个称为局部变量表的变量数组。栈帧中局部变量表的长度在编译期确定，并与该帧所关联方法的代码一起，存储在类或接口的二进制表示中（§4.7.3）。

一个局部变量可以存储 int、float、reference 或 returnAddress 类型的值。一对局部变量可以存储 long 或 double 类型的值。

局部变量通过索引来寻址。第一个局部变量的索引为零。当且仅当一个整数介于零和局部变量表大小减一之间时，它才被视为局部变量表的一个有效索引。

一个 long 或 double 类型的值占据两个连续的局部变量。这种值只能使用较小的那个索引来寻址。例如，一个存储在局部变量表索引 n 处的 double 类型的值，实际上占据了索引为 n 和 n+1 的局部变量；然而，不能直接加载索引 n+1 处的局部变量。但可以向其中存储值。不过，这样做会使局部变量 n 的内容变为无效。

Java 虚拟机不要求 n 是偶数。直观地说，long 和 double 类型的值在局部变量表中不需要是 64 位对齐的。实现者可以自由决定如何使用为该值保留的两个局部变量来表示这些值。

Java 虚拟机使用局部变量在方法调用时传递参数。在调用类方法（静态方法） 时，所有参数从局部变量 0 开始，存入连续的局部变量中。在调用实例方法时，局部变量 0 总是用于传递对调用该实例方法所在对象的引用（即 Java 编程语言中的 this）。随后，所有参数从局部变量 1 开始，存入连续的局部变量中。

#### 2.6.2 操作数栈（Operand Stacks）

每个栈帧（§2.6）都包含一个后进先出的栈，称为其操作数栈。栈帧中操作数栈的最大深度在编译期确定，并与该帧所关联方法的代码一起提供（§4.7.3）。

在上下文明确的情况下，我们有时会将当前帧的操作数栈简称为操作数栈。

当包含操作数栈的帧被创建时，操作数栈是空的。Java 虚拟机提供指令用于将常量或从局部变量、字段中加载的值压入操作数栈。其他 Java 虚拟机指令则从操作数栈中取出操作数，对其进行操作，并将结果压回操作数栈。操作数栈还用于准备传递给方法的参数和接收方法的结果。

例如，iadd 指令（§iadd）将两个 int 值相加。它要求待相加的两个 int 值是操作数栈顶的两个值，并由之前的指令压入。这两个 int 值都会从操作数栈中弹出。它们被相加后，其和会被压回操作数栈。子计算可以在操作数栈上嵌套进行，其结果可供外层的计算使用。

操作数栈上的每个条目可以保存任何 Java 虚拟机类型的值，包括 long 或 double 类型的值。

对操作数栈中的值进行操作时，必须使用符合其类型的方式。例如，不可能压入两个 int 值随后将它们视为一个 long 类型，或者压入两个 float 值随后使用 iadd 指令对它们进行加法操作。有少量 Java 虚拟机指令（如 dup 指令（§dup）和 swap（§swap））将运行时数据区中的值当作原始值进行操作，而不关心其具体类型；这些指令的定义方式确保了它们不能被用来修改或分解单个值。这些对操作数栈操作的约束通过类文件验证（§4.10）来强制执行。

在任何时间点，操作数栈都有一个相关的深度，其中 long 或 double 类型的值贡献两个单位的深度，任何其他类型的值贡献一个单位的深度。

#### 2.6.3 动态链接（Dynamic Linking）

每个栈帧（§2.6）都包含一个指向当前方法所属类型的运行时常量池（§2.5.5）的引用，以支持方法代码的动态链接。方法的类文件代码通过符号引用来引用要调用的方法和要访问的变量。动态链接将这些符号方法引用转换为具体的方法引用，在必要时加载类以解析尚未定义的符号，并将变量访问转换为与这些变量运行时位置相关的存储结构中的适当偏移量。

这种方法和变量的晚期绑定，使得方法所使用的其他类发生变更时，不太可能破坏此代码。

#### 2.6.4 方法调用正常完成（Normal Method Invocation Completion）

如果某个方法调用没有导致异常（§2.10）抛出（无论是直接由 Java 虚拟机抛出，还是由于执行了显式的 throw 语句），那么该次方法调用就正常完成。如果当前方法的调用正常完成，则可能会返回一个值给调用方法。这发生在被调用方法执行了某条 return 指令（§2.11.8）时，所选择的 return 指令必须与待返回值的类型（如果有的话）相匹配。

在这种情况下，将使用当前帧（§2.6）来恢复调用者的状态，包括其局部变量表和操作数栈，同时调用者的程序计数器会适当地递增，以跳过该次方法调用指令。随后，执行将在调用方法的帧中正常继续，返回值（如果有）会被压入该调用者帧的操作数栈中。

#### 2.6.5 方法调用突然完成（Abrupt Method Invocation Completion）

如果方法中的某个 Java 虚拟机指令的执行导致 Java 虚拟机抛出异常（§2.10），并且该异常未在方法内部被处理，那么该方法调用就突然完成。执行 athrow 指令（§athrow）也会显式抛出异常，如果该异常未被当前方法捕获，同样会导致方法调用突然完成。一个突然完成的方法调用永远不会向其调用者返回一个值。

## 2.7 对象的表示（Representation of Objects）

Java 虚拟机并未规定对象的具体内部结构。

在 Oracle 的某些 Java 虚拟机实现中，对一个类实例的引用是一个指向句柄的指针，而该句柄本身包含一对指针：一个指向包含该对象方法的表（方法表）和代表该对象类型的 Class 对象，另一个指向从堆中为对象数据分配的内存。

## 2.8 浮点运算（Floating-Point Arithmetic）

Java 虚拟机包含了 IEEE 754 标准 (JLS §1.7) 中指定的浮点运算的一个子集。

在 Java SE 15 及之后版本中，Java 虚拟机使用 2019 版 IEEE 754 标准。在 Java SE 15 之前，Java 虚拟机使用 1985 版 IEEE 754 标准，其中 binary32 格式称为单精度格式，binary64 格式称为双精度格式。

许多用于算术运算（§2.11.3）和类型转换（§2.11.4）的 Java 虚拟机指令都处理浮点数。除了下面描述的某些指令外，这些指令通常与 IEEE 754 操作（表 2.8-A）相对应。

**表 2.8-A. 与 IEEE 754 操作的对应关系**

| JVM 指令                                               | IEEE 754 操作                                                                                                                   |
| :----------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| `dcmp<op>` (§dcmp`<op>`), `fcmp<op>` (§fcmp`<op>`)     | compareQuietLess, compareQuietLessEqual, compareQuietGreater, compareQuietGreaterEqual, compareQuietEqual, compareQuietNotEqual |
| `dadd` (§dadd), `fadd` (§fadd)                         | addition                                                                                                                        |
| `dsub` (§dsub), `fsub` (§fsub)                         | subtraction                                                                                                                     |
| `dmul` (§dmul), `fmul` (§fmul)                         | multiplication                                                                                                                  |
| `ddiv` (§ddiv), `fdiv` (§fdiv)                         | division                                                                                                                        |
| `dneg` (§dneg), `fneg` (§fneg)                         | negate                                                                                                                          |
| `i2d` (§i2d), `i2f` (§i2f), `l2d` (§l2d), `l2f` (§l2f) | convertFromInt                                                                                                                  |
| `d2i` (§d2i), `d2l` (§d2l), `f2i` (§f2i), `f2l` (§f2l) | convertToIntegerTowardZero                                                                                                      |
| `d2f` (§d2f), `f2d` (§f2d)                             | convertFormat                                                                                                                   |

**Java 虚拟机支持的浮点运算与 IEEE 754 标准之间的主要差异包括：**

- 浮点余数指令 drem（§drem）和 frem（§frem）与 IEEE 754 的余数操作不符。这些指令基于向零舍入策略的隐含除法；而 IEEE 754 余数则基于向最接近值舍入策略的隐含除法。（舍入策略将在下文讨论。）
- 浮点取负指令 dneg（§dneg）和 fneg（§fneg）与 IEEE 754 的取负操作不完全一致。特别是，这些指令不要求反转 NaN 操作数的符号位。
- Java 虚拟机的浮点指令不会抛出异常、陷入陷阱或以其他方式通知 IEEE 754 的异常条件（无效操作、除零、上溢、下溢或不精确）。
- Java 虚拟机不支持 IEEE 754 的信令浮点比较，也没有信令 NaN 值。
- IEEE 754 包含的舍入方向属性与 Java 虚拟机中的舍入策略不对应。Java 虚拟机不提供任何方法来更改给定浮点指令使用的舍入策略。
- Java 虚拟机不支持 IEEE 754 定义的 binary32 扩展和 binary64 扩展浮点格式。在操作或存储浮点值时，不得使用超出 float 和 double 类型规定范围的扩展范围或扩展精度。
- 一些在 Java 虚拟机中没有对应指令的 IEEE 754 操作，通过 Math 和 StrictMath 类中的方法提供，包括用于 IEEE 754 squareRoot 操作的 sqrt 方法、用于 IEEE 754 fusedMultiplyAdd 操作的 fma 方法以及用于 IEEE 754 余数操作的 IEEEremainder 方法。
- Java 虚拟机要求支持 IEEE 754 非规范浮点数和渐进下溢，这使得证明特定数值算法的理想性质更加容易。

**舍入策略**
浮点运算是实数算术的近似。虽然实数有无限多个，但特定的浮点格式只有有限个值。在 Java 虚拟机中，舍入策略是一个用于将实数映射到给定格式的浮点值的函数。

Java 虚拟机定义了两种舍入策略：

1. 向最接近值舍入

   - 适用于除 (i) 转换为整数值和 (ii) 求余之外的所有浮点指令。
   - 不精确的结果必须舍入到最接近无限精确结果的可表示值；如果两个最接近的可表示值距离相等，则选择最低有效位为零的值。
   - 对应于 IEEE 754 中的 roundTiesToEven 舍入方向属性。

2. 向零舍入

   - 适用于 (i) 通过 d2i, d2l, f2i, f2l 指令将浮点值转换为整数值，以及 (ii) 浮点余数指令 drem 和 frem。
   - 不精确的结果被舍入到量值上不大于无限精确结果的最接近可表示值。
   - 对于转换为整数，向零舍入策略等效于截断，即丢弃小数部分的有效位。
   - 对应于 IEEE 754 中的 roundTowardZero 舍入方向属性。

Java 虚拟机要求每条浮点指令都将其浮点结果舍入到目标精度。每条指令使用的舍入策略如上所述，要么是向最接近值舍入，要么是向零舍入。

**严格求值与历史演变**

- Java 1.0 和 1.1 要求对浮点表达式进行严格求值，确保结果完全符合 IEEE 754 标准，具有可预测性，但在某些处理器上导致性能问题。
- Java 1.2 到 Java SE 16 作为折衷方案，允许 JVM 实现为每种浮点类型关联一个或两个值集：

  - float 类型关联 float 值集 和 float-扩展指数值集。
  - double 类型关联 double 值集 和 double-扩展指数值集。
  - 扩展指数值集具有相同的精度位数但更大的指数范围，这在某些处理器上提高了性能。
  - 为了兼容性，可以通过在方法声明上设置 ACC_STRICT 标志来禁止使用扩展指数值集，强制该方法使用严格求值。

- Java SE 17 及以后，随着硬件进步，性能问题已解决。因此，Java SE 平台再次始终要求对浮点表达式进行严格求值。

  - 规范不再将 float 和 double 与上述四个值集关联。
  - ACC_STRICT 标志不再影响浮点运算的求值。
  - 为了兼容性，在主版本号大于 60 的类文件中，原先表示 ACC_STRICT 的位模式未被分配（即不表示任何标志）。未来版本的 Java 虚拟机可能会在未来类文件中为该位模式分配不同的含义。

## 2.9 特殊方法（Special Methods）

#### 2.9.1 实例初始化方法（ Instance Initialization Methods）

一个类包含零个或多个实例初始化方法，每个方法通常对应于用 Java 编程语言编写的一个构造器。

一个方法要成为实例初始化方法，必须同时满足以下所有条件：

- 它在类中定义（而非接口中）。
- 它具有特殊的名称 `<init>`。
- 它的返回类型为 void（§4.3.3）。

在一个类中，任何名为 `<init>` 的非 void 方法都不是实例初始化方法。在一个接口中，任何名为 `<init>` 的方法也都不是实例初始化方法。此类方法无法被任何 Java 虚拟机指令（§4.4.2, §4.9.2）调用，并且会在格式检查（§4.6, §4.8）时被拒绝。

实例初始化方法的声明和使用受到 Java 虚拟机的约束。在声明方面，该方法的 access_flags 项和 code 数组受到约束（§4.6, §4.9.2）。在使用方面，实例初始化方法只能通过 invokespecial 指令在一个未初始化的类实例上调用（§4.10.1.9）。

由于名称 `<init>` 在 Java 编程语言中不是有效的标识符，因此它不能直接在用 Java 编程语言编写的程序中使用。

#### 2.9.2 类或接口初始化方法（Class Initialization Methods）

一个类或接口至多有一个类或接口初始化方法，并且由 Java 虚拟机通过调用该方法（§5.5）来对其进行初始化。

一个方法要成为类或接口初始化方法，必须同时满足以下所有条件：

- 它具有特殊的名称 `<clinit>`。
- 它的返回类型为 void（§4.3.3）。
- 在版本号为 51.0 或以上的类文件中，该方法设置了 ACC_STATIC 标志且不接收任何参数（§4.6）。

对 ACC_STATIC 标志的要求是在 Java SE 7 中引入的，对不接收参数的要求是在 Java SE 9 中引入的。在版本号为 50.0 或以下的类文件中，一个名为 `<clinit>` 且返回类型为 void 的方法即被视为类或接口初始化方法，而不论其 ACC_STATIC 标志的设置情况或是否接收参数。

类文件中其他名为 `<clinit>` 的方法不是类或接口初始化方法。它们永远不会被 Java 虚拟机自身调用，无法被任何 Java 虚拟机指令（§4.9.1）调用，并且会在格式检查（§4.6, §4.8）时被拒绝。

由于名称 `<clinit>` 在 Java 编程语言中不是有效的标识符，因此它不能直接在用 Java 编程语言编写的程序中使用。

#### 2.9.3 签名多态方法（Signature Polymorphic Methods）

一个方法要成为签名多态方法，必须同时满足以下所有条件：

- 它在 java.lang.invoke.MethodHandle 类或 java.lang.invoke.VarHandle 类中声明。
- 它有一个形式参数，其类型为 Object[]。
- 它设置了 ACC_VARARGS 和 ACC_NATIVE 标志。

Java 虚拟机在 invokevirtual 指令（§invokevirtual）中对签名多态方法给予特殊处理，以便实现对方法句柄的调用，或者对由 java.lang.invoke.VarHandle 实例引用的变量进行访问。

一个方法句柄是对底层方法、构造器、字段或类似低级操作（§5.4.3.5）的、动态强类型且可直接执行的引用，并可选择对参数或返回值进行转换。一个 java.lang.invoke.VarHandle 实例是对一个变量或一组变量的动态强类型引用，这组变量可以包括静态字段、非静态字段、数组元素或堆外数据结构的组件。更多信息请参阅 Java SE 平台 API 中的 java.lang.invoke 包。

### 2.10 异常（Exceptions）

在 Java 虚拟机中，异常由 Throwable 类或其子类的实例表示。抛出异常会导致控制流立即从异常抛出点进行非本地转移。

大多数异常是同步发生的，是执行线程的某个操作的直接结果。与之相对，异步异常则可能在程序执行的任何时间点发生。Java 虚拟机抛出异常出于以下三个原因之一：

1. 执行了 athrow 指令（§athrow）。
2. Java 虚拟机同步检测到一个异常的执行条件。这些异常不会在程序的任意点抛出，而只会在执行了某个指令后同步抛出，该指令要么：

   - 将异常指定为可能的结果，例如：
     - 指令执行的操作违反了 Java 编程语言的语义，例如数组索引越界。
     - 在加载或链接程序部分时发生错误。
   - 导致某个资源限制被超出，例如使用了过多内存。

3. 发生了异步异常，因为 Java 虚拟机实现中发生了内部错误（§6.3）。

Java 虚拟机实现可能允许在抛出异步异常之前执行少量但有界的指令。允许这种延迟是为了让优化后的代码能够在实际可行且符合 Java 语言语义的位置检测并抛出这些异常。

**精确异常**

Java 虚拟机抛出的异常是精确的：当控制权转移发生时，在异常抛出点之前执行的所有指令的效果必须看起来都已经发生。在异常抛出点之后的所有指令必须看起来都未曾被执行。如果优化代码已经推测性执行了异常发生点之后的某些指令，那么此类代码必须准备好向用户隐藏这种推测性执行对程序可见状态的影响。

**异常处理**

Java 虚拟机中的每个方法都可以与零个或多个异常处理器相关联。一个异常处理器指定了该方法实现代码中偏移量的范围，表示该处理器在此范围内有效；描述了该异常处理器能够处理的异常类型；并指定了处理该异常的代码位置。

如果导致异常的指令的偏移量在异常处理器的偏移量范围内，并且异常类型与该异常处理器所能处理的异常类是相同类或其子类，则称该异常与该异常处理器匹配。当抛出异常时，Java 虚拟机在当前方法中搜索匹配的异常处理器。如果找到匹配的异常处理器，系统将跳转到该处理器指定的异常处理代码处执行。

如果在当前方法中没有找到这样的异常处理器，则当前方法调用突然完成（§2.6.5）。在突然完成时，当前方法调用的操作数栈和局部变量将被丢弃，其栈帧被弹出，恢复调用者方法的栈帧。然后，异常在调用者方法的栈帧上下文中被重新抛出，并以此类推，沿着方法调用链向上继续搜索。如果在到达方法调用链的顶端之前仍未找到合适的异常处理器，则抛出该异常的线程的执行将被终止。

在线程终止之前，未捕获的异常将根据以下规则进行处理：

- 如果为该线程设置了未捕获异常处理器，则执行该处理器。
- 否则，将调用该线程所属父 ThreadGroup 的 uncaughtException 方法。如果该 ThreadGroup 及其父 ThreadGroup 没有重写 uncaughtException 方法，则调用默认处理器的 uncaughtException 方法。

**异常处理器搜索顺序**

为一个方法搜索匹配异常处理器的顺序非常重要。在类文件中，每个方法的异常处理器存储在一个表（§4.7.3）中。在运行时，当异常被抛出时，Java 虚拟机按照它们在该方法的异常处理器表中出现的顺序（从表头开始）来搜索当前方法的异常处理器。

请注意，Java 虚拟机并不强制要求一个方法的异常表条目之间存在嵌套关系或任何特定顺序。Java 编程语言的异常处理语义仅通过与编译器的协作来实现（§3.12）。当类文件由其他方式生成时，所定义的搜索过程确保了所有 Java 虚拟机实现都能行为一致。

### 2.11 指令集概述（Instruction Set Summary）

Java 虚拟机指令由一个单字节的操作码和零个或多个操作数组成。该操作码指明了要执行的操作，后续的操作数则用于提供该操作所需的参数或数据。许多指令没有操作数，仅由操作码构成。

如果不考虑异常处理，Java 虚拟机解释器的内部循环实质上可以看作是：

```text
do {
    自动计算程序计数器PC;
    获取PC指向的操作码;
    if (存在操作数) 从字节码中获取操作数;
    执行操作码所定义的操作;
} while (解释工作尚未结束);
```

操作数的数量和大小由操作码决定。如果一个操作数的大小超过一个字节，则按照大端序存储——即高位字节在前。例如，一个指向局部变量表的无符号 16 位索引，存储为两个无符号字节 byte1 和 byte2，其值为 (`byte1 << 8`) | byte2。

字节码指令流仅按单字节对齐。只有两个例外情况：lookupswitch 和 tableswitch 指令（§lookupswitch, §tableswitch），它们会进行填充，以强制其内部某些操作数在 4 字节边界上对齐。

将 Java 虚拟机操作码限制为一个字节，并且在编译后的代码中放弃数据对齐，这一决定反映了设计上有意识地偏向于代码的紧凑性，这可能以在某些简单实现中牺牲部分性能为代价。单字节操作码也限制了指令集的大小。不假设数据对齐意味着，在许多机器上，大于一个字节的立即数必须在运行时从字节动态构造。

#### 2.11.1 类型与 Java 虚拟机（Types and the Java Virtual Machine）

Java 虚拟机指令集中的大多数指令都对其所执行操作的类型信息进行了编码。例如，iload 指令（§iload）将一个必须是 int 类型的局部变量内容加载到操作数栈上。而 fload 指令（§fload）则对 float 值执行相同的操作。这两条指令的实现可能完全相同，但拥有不同的操作码。

对于大多数类型化指令，其指令类型通过一个字母在操作码助记符中显式表示：i 代表 int 操作，l 代表 long，s 代表 short，b 代表 byte，c 代表 char，f 代表 float，d 代表 double，a 代表引用类型。一些类型明确的指令在其助记符中没有类型字母。例如，arraylength 总是操作一个数组对象。一些指令，如无条件控制转移指令 goto，则不操作类型化操作数。

鉴于 Java 虚拟机操作码的单字节大小，将类型编码到操作码中给其指令集设计带来了压力。如果每条类型化指令都支持 Java 虚拟机的所有运行时数据类型，那么指令数量将超过一个字节所能表示的范围。因此，Java 虚拟机的指令集对某些操作提供的类型支持水平是经过缩减的。换句话说，指令集有意设计为非正交的。必要时，可以使用单独的指令在不支持和支持的数据类型之间进行转换。

表 2.11.1-A 总结了 Java 虚拟机指令集中的类型支持情况。通过用"类型"列中的字母替换"操作码"列中指令模板的 T，可以构建出带有类型信息的具体指令。如果某个指令模板和类型对应的"类型"列为空，则表示不存在支持该类型操作的指令。例如，有用于 int 类型的加载指令 iload，但没有用于 byte 类型的加载指令。

请注意，表 2.11.1-A 中的大多数指令没有为整数类型 byte、char 和 short 提供形式。所有指令都没有为 boolean 类型提供形式。每当 byte 和 short 类型的值被加载到操作数栈时，它们会通过符号扩展隐式转换为 int 类型的值。同样，每当 boolean 和 char 类型的值被加载到操作数栈时，它们会通过零扩展隐式转换为 int 类型的值。因此，对最初存储在 boolean、byte、char 和 short 类型变量中的值进行的大多数操作，都可以通过操作计算类型 int 的值的指令来正确执行。

**表 2.11.1-A. Java 虚拟机指令集中的类型支持**

| 指令模板      | byte    | short   | int       | long    | float   | double  | char    | reference |
| :------------ | :------ | :------ | :-------- | :------ | :------ | :------ | :------ | :-------- |
| **Tipush**    | bipush  | sipush  |           |         |         |         |         |           |
| **Tconst**    |         |         | iconst    | lconst  | fconst  | dconst  |         | aconst    |
| **Tload**     |         |         | iload     | lload   | fload   | dload   |         | aload     |
| **Tstore**    |         |         | istore    | lstore  | fstore  | dstore  |         | astore    |
| **Tinc**      |         |         | iinc      |         |         |         |         |           |
| **Taload**    | baload  | saload  | iaload    | laload  | faload  | daload  | caload  | aaload    |
| **Tastore**   | bastore | sastore | iastore   | lastore | fastore | dastore | castore | aastore   |
| **Tadd**      |         |         | iadd      | ladd    | fadd    | dadd    |         |           |
| **Tsub**      |         |         | isub      | lsub    | fsub    | dsub    |         |           |
| **Tmul**      |         |         | imul      | lmul    | fmul    | dmul    |         |           |
| **Tdiv**      |         |         | idiv      | ldiv    | fdiv    | ddiv    |         |           |
| **Trem**      |         |         | irem      | lrem    | frem    | drem    |         |           |
| **Tneg**      |         |         | ineg      | lneg    | fneg    | dneg    |         |           |
| **Tshl**      |         |         | ishl      | lshl    |         |         |         |           |
| **Tshr**      |         |         | ishr      | lshr    |         |         |         |           |
| **Tushr**     |         |         | iushr     | lushr   |         |         |         |           |
| **Tand**      |         |         | iand      | land    |         |         |         |           |
| **Tor**       |         |         | ior       | lor     |         |         |         |           |
| **Txor**      |         |         | ixor      | lxor    |         |         |         |           |
| **i2T**       | i2b     | i2s     |           | i2l     | i2f     | i2d     | i2c     |           |
| **l2T**       |         |         | l2i       |         | l2f     | l2d     |         |           |
| **f2T**       |         |         | f2i       | f2l     |         | f2d     |         |           |
| **d2T**       |         |         | d2i       | d2l     | d2f     |         |         |           |
| **Tcmp**      |         |         |           | lcmp    |         |         |         |           |
| **Tcmpl**     |         |         |           |         | fcmpl   | dcmpl   |         |           |
| **Tcmpg**     |         |         |           |         | fcmpg   | dcmpg   |         |           |
| **if_TcmpOP** |         |         | if_icmpOP |         |         |         |         | if_acmpOP |
| **Treturn**   |         |         | ireturn   | lreturn | freturn | dreturn |         | areturn   |

Java 虚拟机存储类型与 Java 虚拟机计算类型之间的映射关系总结在表 2.11.1-B 中。

某些 Java 虚拟机指令（如 pop 和 swap）在操作数栈上进行操作时不关心具体类型；然而，这些指令的使用被限制于仅能操作特定计算类型类别的值，这些类别同样在表 2.11.1-B 中给出。

**表 2.11.1-B. Java 虚拟机中的存储类型和计算类型**

| 存储类型        | 计算类型        | 类别 |
| :-------------- | :-------------- | :--- |
| `boolean`       | `int`           | 1    |
| `byte`          | `int`           | 1    |
| `char`          | `int`           | 1    |
| `short`         | `int`           | 1    |
| `int`           | `int`           | 1    |
| `float`         | `float`         | 1    |
| `reference`     | `reference`     | 1    |
| `returnAddress` | `returnAddress` | 1    |
| `long`          | `long`          | 2    |
| `double`        | `double`        | 2    |

#### 2.11.2 加载和存储指令（Load and Store Instructions）

加载和存储指令在 Java 虚拟机栈帧（§2.6）的局部变量表（§2.6.1）和操作数栈（§2.6.2）之间传输值：

- 将局部变量加载到操作数栈：`iload, iload_<n>, lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>`。
- 将值从操作数栈存储到局部变量：`istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>`。
- 将常量加载到操作数栈：`bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_<i>, lconst_<l>, fconst_<f>, dconst_<d>`。
- 使用更宽的索引访问更多的局部变量，或访问更大的立即操作数：`wide`。

访问对象字段和数组元素（§2.11.5）的指令也会与操作数栈进行数据传输。

上面显示的指令助记符中，带有尖括号内尾随字母的（例如 `iload_<n>`）表示指令族（以 `iload_<n>` 为例，其成员包括 iload_0, iload_1, iload_2, iload_3）。这类指令族是一个需要一个操作数的通用指令（iload）的特化版本。对于这些特化指令，操作数是隐式的，无需存储或获取。除此之外的语义是相同的（iload_0 的含义与操作数为 0 的 iload 指令相同）。尖括号内的字母指定了该指令族隐式操作数的类型：`<n>` 表示非负整数；`<i>` 表示 int；`<l>` 表示 long；`<f>` 表示 float；`<d>` 表示 double。

本规范全文都使用这种表示法来描述指令族。

#### 2.11.3 算术指令（Arithmetic Instructions）

算术指令计算一个结果，该结果通常是操作数栈上两个值的函数，并将结果压回操作数栈。算术指令主要有两种：操作整数值的指令和操作浮点值的指令。在这两种类型中，算术指令都针对 Java 虚拟机数值类型进行了专门化。对于 byte、short 和 char 类型（§2.11.1）的值，或者 boolean 类型的值，没有直接的整数算术支持；这些操作由操作 int 类型的指令处理。整数和浮点指令在溢出和除零时的行为也有所不同。算术指令如下：

- 加法：iadd, ladd, fadd, dadd。
- 减法：isub, lsub, fsub, dsub。
- 乘法：imul, lmul, fmul, dmul。
- 除法：idiv, ldiv, fdiv, ddiv。
- 取余：irem, lrem, frem, drem。
- 取负：ineg, lneg, fneg, dneg。
- 移位：ishl, ishr, iushr, lshl, lshr, lushr。
- 按位或：ior, lor。
- 按位与：iand, land。
- 按位异或：ixor, lxor。
- 局部变量自增：iinc。
- 比较：dcmpg, dcmpl, fcmpg, fcmpl, lcmp。

Java 编程语言操作符对整数和浮点数值的语义（JLS §4.2.2, JLS §4.2.4）直接由 Java 虚拟机指令集的语义支持。

**整数运算**

Java 虚拟机在整数数据类型运算期间不指示溢出。唯一能抛出异常的整数运算是整数除法指令（idiv 和 ldiv）和整数取余指令（irem 和 lrem），如果除数为零，它们会抛出 ArithmeticException。

**浮点运算**

Java 虚拟机在浮点数据类型运算期间不指示溢出或下溢。也就是说，浮点指令永远不会导致 Java 虚拟机抛出运行时异常（不要与 IEEE 754 浮点异常混淆）。溢出操作会产生一个有符号的无穷大；下溢操作会产生一个非规范数或有符号的零；没有唯一定义的数学结果的操作会产生 NaN。所有以 NaN 作为操作数的数值运算都会产生 NaN 作为结果。

**比较操作**

- 对 long 类型值的比较（lcmp）执行有符号比较。
- 对浮点类型值的比较（dcmpg, dcmpl, fcmpg, fcmpl）使用 IEEE 754 非信令比较 执行。

#### 2.11.4 类型转换指令（Type Conversion Instructions）

类型转换指令允许在 Java 虚拟机数值类型之间进行转换。这些指令可用于实现用户代码中的显式转换，或弥补 Java 虚拟机指令集缺乏正交性的问题。

**宽化类型转换**

Java 虚拟机直接支持以下宽化数值转换：

- int 转换为 long、float 或 double
- long 转换为 float 或 double
- float 转换为 double

宽化数值转换指令是：i2l, i2f, i2d, l2f, l2d, f2d。鉴于类型化指令的命名约定以及使用 2 表示"转换为"的双关用法，这些操作码的助记符是直观的。例如，i2d 指令将 int 值转换为 double。

大多数宽化数值转换不会丢失数值总体量级的信息。实际上，从 int 到 long 以及从 int 到 double 的转换完全不会丢失任何信息；数值被精确保留。从 float 到 double 的转换也精确地保留了数值。

从 int 到 float、从 long 到 float 或从 long 到 double 的转换可能会丢失精度，即可能会丢失值的一些最低有效位；生成的浮点值是使用向最接近值舍入策略（§2.8）对整数值进行正确舍入后的版本。

尽管可能发生精度损失，但宽化数值转换永远不会导致 Java 虚拟机抛出运行时异常（不要与 IEEE 754 浮点异常混淆）。

int 到 long 的宽化数值转换只是将 int 值的二进制补码表示进行符号扩展以填充更宽的格式。char 到整数类型的宽化数值转换将 char 值的表示进行零扩展以填充更宽的格式。

请注意，不存在从整数类型 byte、char 和 short 到 int 类型的宽化数值转换。如 §2.11.1 所述，byte、char 和 short 类型的值在内部被宽化为 int 类型，使得这些转换是隐式的。

**窄化类型转换**

- Java 虚拟机也直接支持以下窄化数值转换：
- int 转换为 byte、short 或 char。
- long 转换为 int
- float 转换为 int 或 long
- double 转换为 int、long 或 float

窄化数值转换指令是：i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f。窄化数值转换可能导致不同符号、不同数量级或两者兼有的值；因此它可能会丢失精度。

int 或 long 到整数类型 T 的窄化数值转换直接丢弃除最低位的 n 位之外的所有位，其中 n 是用于表示类型 T 的位数。这可能导致结果值的符号与输入值的符号不同。

在将浮点值窄化转换为整数类型 T（T 为 int 或 long）时，按以下规则进行转换：

1. 如果浮点值是 NaN，则转换结果为 int 或 long 类型的 0。
2. 否则，如果浮点值不是无穷大，则使用向零舍入策略（§2.8）将浮点值舍入为整数值 V。有两种情况：

   - 如果 T 是 long 且此整数值可以表示为 long，则结果为 long 值 V。
   - 如果 T 是 int 且此整数值可以表示为 int，则结果为 int 值 V。

3. 否则：

   - 要么该值太小（负的大数值或负无穷大），则结果为 int 或 long 类型的最小可表示值。
   - 要么该值太大（正的大数值或正无穷大），则结果为 int 或 long 类型的最大可表示值。

从 double 到 float 的窄化数值转换遵循 IEEE 754 标准。结果使用向最接近值舍入策略（§2.8）正确舍入。太小而无法表示为 float 的值被转换为 float 类型的正零或负零；太大而无法表示为 float 的值被转换为正无穷大或负无穷大。double 类型的 NaN 总是被转换为 float 类型的 NaN。

尽管可能发生溢出、下溢或精度损失，但数值类型之间的窄化转换永远不会导致 Java 虚拟机抛出运行时异常（不要与 IEEE 754 浮点异常混淆）。

#### 2.11.5 对象创建与操作（Object Creation and Manipulation）

尽管类实例和数组都是对象，但 Java 虚拟机使用不同的指令集来创建和操作类实例与数组：

- 创建新的类实例：new。
- 创建新数组：newarray, anewarray, multianewarray。
- 访问类的字段（静态字段，即类变量）和类实例的字段（非静态字段，即实例变量）：getstatic, putstatic, getfield, putfield。
- 将数组组件加载到操作数栈：baload, caload, saload, iaload, laload, faload, daload, aaload。
- 将操作数栈中的值存储为数组组件：bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore。
- 获取数组长度：arraylength。
- 检查类实例或数组的属性：instanceof, checkcast。

#### 2.11.6 操作数栈管理指令（Operand Stack Management Instructions）

提供了一些指令用于直接操作操作数栈：pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap。

#### 2.11.7 控制转移指令（Control Transfer Instructions）

控制转移指令有条件或无条件地导致 Java 虚拟机不再执行紧跟在控制转移指令之后的指令，而是跳转到另一个指令继续执行。它们包括：

- 条件分支：ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt, if_icmpge, if_acmpeq, if_acmpne。
- 符合条件分支：tableswitch, lookupswitch。
- 无条件分支：goto, goto_w, jsr, jsr_w, ret。

Java 虚拟机拥有不同的指令集，用于在与 int 类型数据比较和与引用类型数据比较时进行条件分支。它还拥有专门的用于测试 null 引用的条件分支指令，因此无需为 null 指定一个具体值（§2.4）。

对 boolean、byte、char 和 short 类型数据之间比较的条件分支，是使用 int 比较指令（§2.11.1）来执行的。对 long、float 或 double 类型数据之间比较的条件分支，首先使用一条指令比较数据并产生一个表示比较结果的 int 值（§2.11.3），随后的一条 int 比较指令测试这个结果并执行条件分支。由于侧重于 int 比较，Java 虚拟机为 int 类型提供了丰富的条件分支指令。

所有 int 条件控制转移指令都执行有符号比较。

#### 2.11.8 方法调用和返回指令（Method Invocation and Return Instructions）

以下五条指令用于调用方法：

- invokevirtual： 调用对象的实例方法，根据对象的（虚）类型进行动态分派。这是最常见的实例方法调用方式。
- invokeinterface： 调用接口方法，搜索特定运行时对象实现的方法，以找到合适的方法。
- invokespecial： 调用需要特殊处理的实例方法，包括实例初始化方法（`<init>`，即构造器，§2.9.1）、私有方法或当前类的父类方法（通过 super 调用）。
- invokestatic： 调用指定类的类（静态）方法。
- invokedynamic： 调用与该 invokedynamic 指令所绑定的调用点对象所指向的方法。该调用点对象是在该指令首次执行前，由 Java 虚拟机通过运行一个引导方法而绑定到该 invokedynamic 指令的特定词法出现位置的。因此，每条 invokedynamic 指令都有一个唯一的链接状态，这与其他调用方法的指令不同。

方法返回指令根据返回类型区分，包括 ireturn（用于返回 boolean、byte、char、short 和 int 类型）、lreturn、freturn、dreturn 和 areturn。此外，return 指令用于从声明为 void 的方法、实例初始化方法以及类或接口初始化方法中返回。

#### 抛出异常（Throwing Exceptions）

可以使用 athrow 指令以编程方式抛出异常。如果检测到异常条件，各种 Java 虚拟机指令也会抛出异常。

#### 2.11.10 同步（Synchronization）

Java 虚拟机通过单一的同步结构——监视器，来支持对方法以及方法内部指令序列的同步。

**方法级同步**

方法级同步是隐式执行的，作为方法调用和返回（§2.11.8）的一部分。一个同步方法在运行时常量池的 method_info 结构（§4.6）中通过 ACC_SYNCHRONIZED 标志来标识，该方法调用指令会检查此标志。当调用一个设置了 ACC_SYNCHRONIZED 的方法时，执行线程会进入监视器，调用方法本身，然后无论方法调用是正常完成还是突然完成，都会退出监视器。在执行线程拥有监视器期间，没有其他线程可以进入该监视器。如果在调用同步方法期间抛出异常，并且该同步方法没有处理该异常，那么在异常被重新抛出到同步方法之外之前，该方法的监视器会被自动退出。

**指令序列同步**

指令序列的同步通常用于实现 Java 编程语言中的 synchronized 语句块。Java 虚拟机提供了 monitorenter 和 monitorexit 指令来支持此类语言结构。正确实现同步块需要面向 Java 虚拟机的编译器的协作（§3.14）。

**结构化锁定**

结构化锁定 是指在一次方法调用期间，对给定监视器的每一次退出都匹配一次先前的进入。由于无法保证所有提交给 Java 虚拟机的代码都会执行结构化锁定，Java 虚拟机实现允许但不强制要求强制执行以下两条保证结构化锁定的规则。设 T 为一个线程，M 为一个监视器：

1. 在一次方法调用期间，T 对 M 执行的监视器进入次数必须等于 T 对 M 执行的监视器退出次数，无论该方法是正常完成还是突然完成。
2. 在一次方法调用期间的任何时间点，T 自方法调用开始以来对 M 执行的监视器退出次数都不得超过自方法调用开始以来对 M 执行的监视器进入次数。

请注意，在调用同步方法时，由 Java 虚拟机自动执行的监视器进入和退出被视为发生在调用方法的调用期间。

### 2.12 类库（Class Libraries）

Java 虚拟机必须为 Java SE 平台类库的实现提供充分的支持。这些库中的某些类，若没有 Java 虚拟机的协作则无法实现。

可能需要 Java 虚拟机特殊支持的类包括那些支持以下功能的类：

- 反射，例如 java.lang.reflect 包中的类以及 Class 类。
- 类或接口的加载和创建。最明显的例子是 ClassLoader 类。
- 类或接口的链接和初始化。上面引用的示例类也属于此类别。
- 模块系统，例如 ModuleLayer 类。
- 多线程，例如 Thread 类。
- 弱引用，例如 java.lang.ref 包中的类。

上面的列表旨在说明而非穷尽。详细列出这些类或其提供的功能超出了本规范的范围。有关详细信息，请参阅 Java SE 平台类库的规范。

### 2.13 公开设计与私有实现（Public Design, Private Implementation）

至此，本规范已经勾勒出了 Java 虚拟机的公开视图：类文件格式和指令集。这些组件对于 Java 虚拟机的硬件、操作系统和实现独立性至关重要。实现者不妨将它们视为在不同主机（每台主机都实现了 Java SE 平台）之间安全地传输程序片段的一种手段，而不是一个需要严格遵循的蓝图。

理解公开设计与私有实现之间的界限非常重要。Java 虚拟机实现必须能够读取类文件，并且必须精确实现其中所包含的 Java 虚拟机代码的语义。实现此目标的一种方法是，将本文档视为规范并严格按字面意思实现该规范。但是，实现者在本规范的约束范围内修改或优化实现也是完全可行且可取的。只要能够读取类文件格式并保持其代码的语义，实现者可以以任何方式实现这些语义。"底层"的实现细节是实现者自己的事情，只要仔细维护了正确的外部接口即可。

存在一些例外情况：调试器、性能分析器和即时编译器都可能需要访问通常被视为"底层"的 Java 虚拟机元素。在适当的情况下，Oracle 会与其他 Java 虚拟机实现者和工具供应商合作，为此类工具开发访问 Java 虚拟机的通用接口，并在整个行业推广这些接口。

实现者可以利用这种灵活性来定制 Java 虚拟机实现，以实现高性能、低内存占用或可移植性。在特定实现中采用何种方案取决于该实现的目标。实现选项的范围包括：

- 在加载时或执行期间将 Java 虚拟机代码翻译成另一个虚拟机的指令集。
- 在加载时或执行期间将 Java 虚拟机代码翻译成宿主 CPU 的本地指令集（有时称为即时编译或 JIT 代码生成）。

一个精确定义的虚拟机和目标文件格式的存在，并不需要显著限制实现者的创造力。Java 虚拟机旨在支持许多不同的实现，在保持实现之间兼容性的同时，提供新颖且有趣的解决方案。
